package cn.iocoder.yudao.module.promotion.coupon.service;

import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.business.basic.pojo.vo.UserSimpleVO;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.exception.Assertions;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.api.member.user.MemberUserApi;
import cn.iocoder.yudao.module.api.promotion.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.api.promotion.coupon.enums.CouponStatusEnum;
import cn.iocoder.yudao.module.api.promotion.coupon.enums.CouponTakeTypeEnum;
import cn.iocoder.yudao.module.api.promotion.coupon.enums.CouponTemplateValidityTypeEnum;
import cn.iocoder.yudao.module.api.shop.staff.ShopStaffApi;
import cn.iocoder.yudao.module.api.shop.staff.vo.ShopStaffVO;
import cn.iocoder.yudao.module.api.trade.verify.TradeVerifyRecordApi;
import cn.iocoder.yudao.module.api.trade.verify.dto.TradeVerifyRecordSaveDTO;
import cn.iocoder.yudao.module.api.trade.verify.enums.VerifyBizType;
import cn.iocoder.yudao.module.promotion.coupon.convert.CouponRecordConvert;
import cn.iocoder.yudao.module.promotion.coupon.dal.dataobject.CouponRecordDO;
import cn.iocoder.yudao.module.promotion.coupon.dal.dataobject.CouponTemplateDO;
import cn.iocoder.yudao.module.promotion.coupon.dal.mysql.CouponRecordMapper;
import cn.iocoder.yudao.module.promotion.coupon.vo.CouponRecordPageReqVO;
import jakarta.annotation.Nullable;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.module.api.promotion.ErrorCodeConstants.*;
import static java.util.Arrays.asList;

/**
 * 优惠劵领取记录 Service 实现类
 *
 * @author 芋道源码
 */
@Slf4j
@Service
@Validated
public class CouponRecordServiceImpl implements CouponRecordService {

    @Resource
    private CouponTemplateService couponTemplateService;

    @Resource
    private CouponRecordMapper couponRecordMapper;

    @Resource
    @Lazy // 延迟加载，避免循环依赖
    private MemberUserApi memberUserApi;
    @Resource
    @Lazy // 延迟加载，避免循环依赖
    private ShopStaffApi shopStaffApi;
    @Resource
    @Lazy // 延迟加载，避免循环依赖
    private TradeVerifyRecordApi tradeVerifyRecordApi;

    @Override
    public void useCoupon(Long id, Long userId, Long orderId) {
        // 校验状态
        CouponRecordDO coupon = couponRecordMapper.selectByIdAndUserId(id, userId);
        if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.UNUSED.getStatus())) {
            throw exception(COUPON_STATUS_NOT_UNUSED);
        }
        // 校验有效期；为避免定时器没跑，实际优惠劵已经过期
        if (!LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) {
            throw exception(COUPON_VALID_TIME_NOT_NOW);
        }
        // 更新状态
        CouponRecordDO couponRecordDO = new CouponRecordDO();
        couponRecordDO.setStatus(CouponStatusEnum.USED.getStatus());
        couponRecordDO.setUseOrderId(orderId);
        couponRecordDO.setUseTime(LocalDateTime.now());
        int updateCount = couponRecordMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(), couponRecordDO);
        if (updateCount == 0) {
            throw exception(COUPON_STATUS_NOT_UNUSED);
        }
    }

    @Override
    public void returnUsedCoupon(Long id) {
        // 校验存在
        CouponRecordDO coupon = couponRecordMapper.selectById(id);
        if (coupon == null) {
            throw exception(COUPON_NOT_EXISTS);
        }
        // 校验状态
        if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) {
            throw exception(COUPON_STATUS_NOT_USED);
        }

        // 退还
        Integer status = LocalDateTimeUtils.beforeNow(coupon.getValidEndTime())
                ? CouponStatusEnum.EXPIRED.getStatus() // 退还时可能已经过期了
                : CouponStatusEnum.UNUSED.getStatus();
        CouponRecordDO couponRecordDO = new CouponRecordDO();
        couponRecordDO.setStatus(status);
        int updateCount = couponRecordMapper.updateByIdAndStatus(id, CouponStatusEnum.USED.getStatus(), couponRecordDO);
        if (updateCount == 0) {
            throw exception(COUPON_STATUS_NOT_USED);
        }

        // TODO 增加优惠券变动记录？
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteCoupon(Long id) {
        // 校验存在
        CouponRecordDO coupon = validateCouponExists(id);

        // 更新优惠劵
        int deleteCount = couponRecordMapper.delete(id,
                asList(CouponStatusEnum.UNUSED.getStatus(), CouponStatusEnum.EXPIRED.getStatus()));
        if (deleteCount == 0) {
            throw exception(COUPON_DELETE_FAIL_USED);
        }

        // 减少优惠劵模板的领取数量 -1
        couponTemplateService.updateCouponTemplateTakeCount(coupon.getTemplateId(), -1);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<Long, List<Long>> takeCoupon(Long templateId, Set<Long> userIds, @Nullable CouponTakeTypeEnum takeType) {
        CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId);
        return takeCoupon(template, userIds, takeType);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<Long, List<Long>> takeCoupon(CouponTemplateDO template, Set<Long> userIds, @Nullable CouponTakeTypeEnum takeType) {
        if (takeType == null) {
            takeType = CouponTakeTypeEnum.valueOf(template.getTakeType());
        }
        // 1. 过滤掉达到领取限制的用户
        removeTakeLimitUser(userIds, template);
        // 2. 校验优惠劵是否可以领取
        validateCouponTemplateCanTake(template, userIds, takeType);

        // 3. 批量保存优惠劵
        List<CouponRecordDO> couponList = convertList(userIds, userId -> CouponRecordConvert.INSTANCE.convert(template, userId));
        couponList.forEach(coupon -> coupon.setShopId(template.getShopId()));
        couponRecordMapper.insertBatch(couponList);

        // 4. 增加优惠劵模板的领取数量
        couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size());

        return convertMultiMap(couponList, CouponRecordDO::getUserId, CouponRecordDO::getId);
    }

    @Override
    public List<Long> takeCouponsByAdmin(Map<Long, Integer> giveCoupons, Long userId) {
        if (CollUtil.isEmpty(giveCoupons)) {
            return Collections.emptyList();
        }
        List<Long> couponIds = new ArrayList<>();
        // 循环发放
        for (Map.Entry<Long, Integer> entry : giveCoupons.entrySet()) {
            try {
                for (int i = 0; i < entry.getValue(); i++) {
                    Map<Long, List<Long>> userCouponIdsMap = getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId), null);
                    findAndThen(userCouponIdsMap, userId, couponIds::addAll);
                }
            } catch (Exception e) {
                log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败 userId({})]", entry, userId, e);
            }
        }
        return couponIds;
    }

    @Override
    public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
        // 循环收回
        for (Long couponId : giveCouponIds) {
            try {
                getSelf().invalidateCoupon(couponId, userId);
            } catch (Exception e) {
                log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e);
            }
        }
    }

    /**
     * 收回优惠券
     *
     * @param couponId 模版编号
     * @param userId   用户编号
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) // 每次调用开启一个新的事务，避免在一个大的事务里面
    public void invalidateCoupon(Long couponId, Long userId) {
        if (couponId == null || couponId <= 0) {
            return;
        }
        // 1.1 校验优惠券
        CouponRecordDO coupon = couponRecordMapper.selectByIdAndUserId(couponId, userId);
        if (coupon == null) {
            throw exception(COUPON_NOT_EXISTS);
        }
        // 1.2 校验模板
        CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(coupon.getTemplateId());
        if (couponTemplate == null) {
            throw exception(COUPON_TEMPLATE_NOT_EXISTS);
        }
        // 1.3 校验优惠券是否已经使用，如若使用则先不管
        if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) {
            log.info("[invalidateCoupon][coupon({}) 已经使用，无法作废]", couponId);
            return;
        }

        // 2.1 减少优惠劵模板的领取数量
        couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1);
        // 2.2 作废优惠劵
        couponRecordMapper.deleteById(couponId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void takeCouponByRegister(Long userId) {
        List<CouponTemplateDO> templates = couponTemplateService.getEnabledCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER);
        for (CouponTemplateDO template : templates) {
            takeCoupon(template, CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER);
        }
    }

    @Override
    public int expireCoupon() {
        // 1. 查询已过期的优惠券
        List<CouponRecordDO> list = couponRecordMapper.selectListByStatusAndValidEndTimeLe(
                CouponStatusEnum.UNUSED.getStatus(), LocalDateTime.now());
        if (CollUtil.isEmpty(list)) {
            return 0;
        }

        // 2. 遍历执行
        int count = 0;
        for (CouponRecordDO coupon : list) {
            try {
                boolean success = expireCoupon(coupon);
                if (success) {
                    count++;
                }
            } catch (Exception e) {
                log.error("[expireCoupon][coupon({}) 更新为已过期失败]", coupon.getId(), e);
            }
        }
        return count;
    }

    /**
     * 过期单个优惠劵
     *
     * @param coupon 优惠劵
     * @return 是否过期成功
     */
    private boolean expireCoupon(CouponRecordDO coupon) {
        // 更新记录状态
        CouponRecordDO couponRecordDO = new CouponRecordDO();
        couponRecordDO.setStatus(CouponStatusEnum.EXPIRED.getStatus());
        int updateRows = couponRecordMapper.updateByIdAndStatus(coupon.getId(), CouponStatusEnum.UNUSED.getStatus(), couponRecordDO);
        if (updateRows == 0) {
            log.error("[expireCoupon][coupon({}) 更新为已过期失败]", coupon.getId());
            return false;
        }
        log.info("[expireCoupon][coupon({}) 更新为已过期成功]", coupon.getId());
        return true;
    }

    /**
     * 校验优惠券是否可以领取
     *
     * @param couponTemplate 优惠券模板
     * @param userIds        领取人列表
     * @param takeType       领取方式
     */
    private void validateCouponTemplateCanTake(CouponTemplateDO couponTemplate, Set<Long> userIds, CouponTakeTypeEnum takeType) {
        // 如果所有用户都领取过，则抛出异常
        if (CollUtil.isEmpty(userIds)) {
            throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE);
        }
        // 校验模板
        if (couponTemplate == null) {
            throw exception(COUPON_TEMPLATE_NOT_EXISTS);
        }
        // 校验领取方式
        if (ObjUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) {
            throw exception(COUPON_TEMPLATE_CANNOT_TAKE);
        }
        // 校验发放数量不能过小（仅在 CouponTakeTypeEnum.USER 用户领取时）
        if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
            && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制
            && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
            throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
        }
        // 校验"固定日期"的有效期类型是否过期
        if (CouponTemplateValidityTypeEnum.DATE.getType().equals(couponTemplate.getValidityType())) {
            if (LocalDateTimeUtils.beforeNow(couponTemplate.getValidEndTime())) {
                throw exception(COUPON_TEMPLATE_EXPIRED);
            }
        }
    }

    /**
     * 过滤掉达到领取上线的用户
     *
     * @param userIds        用户编号数组
     * @param couponTemplate 优惠劵模板
     */
    private void removeTakeLimitUser(Set<Long> userIds, CouponTemplateDO couponTemplate) {
        if (couponTemplate.getTakeLimitCount() <= 0) {
            return;
        }
        // 查询已领过券的用户
        List<CouponRecordDO> alreadyTakeCoupons = couponRecordMapper.selectListByTemplateIdAndUserId(couponTemplate.getId(), userIds);
        if (CollUtil.isEmpty(alreadyTakeCoupons)) {
            return;
        }
        // 移除达到领取限制的用户
        Map<Long, Integer> userTakeCountMap = CollStreamUtil.groupBy(alreadyTakeCoupons, CouponRecordDO::getUserId, Collectors.summingInt(c -> 1));
        userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount());
    }

    //======================= 查询相关 =======================

    @Override
    public int getCouponCount(Long userId, @Nullable Integer status) {
        Long aLong = couponRecordMapper.selectCountByUserIdAndStatus(userId, status);
        if (aLong == null) {
            return 0;
        }
        return aLong.intValue();
    }

    @Override
    public PageResult<CouponRecordDO> getCouponPage(CouponRecordPageReqVO pageReqVO) {
        // 获得用户编号
        if (StrUtil.isNotEmpty(pageReqVO.getNickname())) {
            List<UserSimpleVO> users = memberUserApi.getUserListByNickname(pageReqVO.getNickname());
            if (CollUtil.isEmpty(users)) {
                return PageResult.empty();
            }
            pageReqVO.setUserIds(convertSet(users, UserSimpleVO::getId));
        }
        // 分页查询
        return couponRecordMapper.selectPage(pageReqVO);
    }

    @Override
    public List<CouponRecordDO> getCouponList(Long userId, @Nullable Integer status) {
        return couponRecordMapper.selectListByUserIdAndStatus(userId, status);
    }

    @Override
    public List<CouponRecordDO> getUnusedCouponList(Long userId, List<Integer> scopes) {
        return couponRecordMapper.selectUnusedListByUserId(userId, scopes);
    }

    @Override
    public Map<Long, Integer> getTakeCountMapByTemplateIds(Collection<Long> templateIds, Long userId) {
        if (CollUtil.isEmpty(templateIds)) {
            return Collections.emptyMap();
        }
        return couponRecordMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds);
    }

    @Override
    public Map<Long, Boolean> getUserCanCanTakeMap(Long userId, List<CouponTemplateDO> templates) {
        // 1. 未登录时，都显示可以领取
        Map<Long, Boolean> userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true);
        if (userId == null) {
            return userCanTakeMap;
        }

        // 2.1 过滤领取数量无限制的
        Set<Long> templateIds = convertSet(templates, CouponTemplateDO::getId, template -> template.getTakeLimitCount() != -1);
        // 2.2 检查用户领取的数量是否超过限制
        if (CollUtil.isNotEmpty(templateIds)) {
            Map<Long, Integer> couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId);
            for (CouponTemplateDO template : templates) {
                Integer takeCount = couponTakeCountMap.get(template.getId());
                userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount());
            }
        }
        return userCanTakeMap;
    }

    @Override
    public CouponRecordDO getCoupon(Long userId, Long id) {
        return couponRecordMapper.selectByIdAndUserId(id, userId);
    }

    @Override
    public Map<Long, Boolean> getCouponReceivedStateMap(Long userId, Set<Long> templateIds) {
        if (userId == null || userId <= 0 || CollUtil.isEmpty(templateIds)) {
            return new HashMap<>();
        }
        Set<Long> ids = couponRecordMapper.selectTemplateIdsByUserIdAndTemplateIds(userId, templateIds);
        Map<Long, Boolean> map = new HashMap<>(templateIds.size());
        templateIds.forEach(id -> map.put(id, ids.contains(id)));
        return map;
    }

    private CouponRecordDO validateCouponExists(Long id) {
        CouponRecordDO coupon = couponRecordMapper.selectById(id);
        if (coupon == null) {
            throw exception(COUPON_NOT_EXISTS);
        }
        return coupon;
    }

    @Override
    public void verifyByAdmin(Long adminUserId, String verifyCode) {
        CouponRecordDO coupon = couponRecordMapper.selectByProductScopeValues(verifyCode);
        Assertions.notNull(coupon, "优惠券领取记录不存在");
        Assertions.notEmpty(coupon.getProductScopeValues(), "优惠券领取记录没有核销码");
        ShopStaffVO verifyStaffInfo = shopStaffApi.getByUserId(coupon.getShopId(), adminUserId, UserTypeEnum.ADMIN.getValue());
        Assertions.notNull(verifyStaffInfo, "没有该优惠券领取记录的核销权限");
        // 核销记录
        getSelf().saveVerifyRecord(verifyStaffInfo.getContactName(), verifyStaffInfo.getContactMobile(), coupon);
    }

    @Override
    public void verifyByMerchant(Long memberUserId, String verifyCode) {
        CouponRecordDO coupon = couponRecordMapper.selectByProductScopeValues(verifyCode);
        Assertions.notNull(coupon, "优惠券领取记录不存在");
        Assertions.notEmpty(coupon.getProductScopeValues(), "优惠券领取记录没有核销码");
        ShopStaffVO verifyStaffInfo = shopStaffApi.getByUserId(coupon.getShopId(), memberUserId, UserTypeEnum.MEMBER.getValue());
        Assertions.notNull(verifyStaffInfo, "没有该优惠券领取记录的核销权限");
        // 核销记录
        getSelf().saveVerifyRecord(verifyStaffInfo.getContactName(), verifyStaffInfo.getContactMobile(), coupon);
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveVerifyRecord(String verifierName, String verifierMobile, CouponRecordDO coupon) {
        TradeVerifyRecordSaveDTO saveDTO = new TradeVerifyRecordSaveDTO();
        saveDTO.setUserId(coupon.getUserId());
        saveDTO.setShopId(coupon.getShopId());
        saveDTO.setCode(StrUtil.join(",", coupon.getProductScopeValues()));
        saveDTO.setBizType(VerifyBizType.COUPON_RECORD.getValue());
        saveDTO.setBizId(String.valueOf(coupon.getTemplateId()));
        saveDTO.setVerifierName(verifierName);
        saveDTO.setVerifierMobile(verifierMobile);
        saveDTO.setNote(coupon.getName());
        tradeVerifyRecordApi.createVerifyRecord(saveDTO);
        CouponRecordDO updateObj = new CouponRecordDO();
        updateObj.setId(coupon.getId());
        updateObj.setStatus(CouponStatusEnum.USED.getStatus());
        couponRecordMapper.updateById(updateObj);
    }

    @Override
    public CouponRespDTO getByVerifyCode(Long userId, Integer userType, String verifyCode) {
        CouponRecordDO coupon = couponRecordMapper.selectByProductScopeValues(verifyCode);
        Assertions.notNull(coupon, "优惠券领取记录不存在");
        ShopStaffVO verifyStaffInfo = shopStaffApi.getByUserId(coupon.getShopId(), userId, userType);
        Assertions.notNull(verifyStaffInfo, "没有该优惠券领取记录的核销权限");
        return CouponRecordConvert.INSTANCE.convert(coupon);
    }

    /**
     * 获得自身的代理对象，解决 AOP 生效问题
     *
     * @return 自己
     */
    private CouponRecordServiceImpl getSelf() {
        return SpringUtil.getBean(getClass());
    }

}
